﻿using CocoaAni.Net.WebSockets.Enums;

namespace CocoaAni.Net.WebSockets.Frames;

public class SendFrame : Frame
{
    public SendFrame(
        byte[] data, int offset, int dataLen,
        SendFrameBuildMode saveMode,
        bool isFin, WSOpcode opcode, byte[]? masks) : base(0)
    {
        var headLen = (dataLen <= 125 ? 2 : dataLen <= 0xFFFF ? 4 : 10) + (masks == null ? 0 : 4);
        var dataId = 0;
        var canDoMove = false;
        switch (saveMode)
        {
            case SendFrameBuildMode.MoveOrSplit:
                canDoMove = data.Length - dataLen > headLen;
                if (canDoMove)
                    goto case SendFrameBuildMode.Move;
                else
                    goto case SendFrameBuildMode.Split;

            case SendFrameBuildMode.MoveOrNewBuffer:
                canDoMove = data.Length - dataLen > headLen;
                if (canDoMove)
                    goto case SendFrameBuildMode.Move;
                else
                    goto case SendFrameBuildMode.NewBuffer;

            case SendFrameBuildMode.Move:
                if (!canDoMove)
                {
                    canDoMove = data.Length - dataLen > headLen;
                    if (!canDoMove)
                        throw new IndexOutOfRangeException
                            ($"data Size = {data.Length}  Should > {headLen + dataLen} !");
                }
                Buffer = data;
                HeaderBuffer = data;
                HeaderStartIndex = offset;
                PayloadLength = dataLen;
                PayloadStartIndex = offset + headLen;
                var end = PayloadStartIndex + dataLen;
                if (masks != null)
                {
                    for (var i = end; i >= PayloadStartIndex; i++, dataId++)
                        data[i] = (byte)(data[i - 1] ^ masks[dataId % 4]);
                }
                else
                {
                    for (var i = end; i >= PayloadStartIndex; i++)
                        data[i] = data[i - 1];
                }
                IsFinal = isFin;
                Opcode = opcode;
                Masks = masks;
                break;

            case SendFrameBuildMode.Split:
                HeaderBuffer = new byte[headLen];
                Buffer = data;
                IsFinal = isFin;
                Opcode = opcode;
                Masks = masks;
                HeaderStartIndex = 0;
                PayloadLength = dataLen;
                PayloadStartIndex = offset;
                end = offset + dataLen;
                if (masks == null)
                    break;
                for (var i = PayloadStartIndex; i < end; i++, dataId++)
                {
                    data[i] = (byte)(data[i] ^ masks[dataId % 4]);
                }
                break;

            case SendFrameBuildMode.NewBuffer:
                var newBuffer = new byte[headLen + dataLen];
                HeaderBuffer = newBuffer;
                Buffer = newBuffer;
                IsFinal = isFin;
                Opcode = opcode;
                HasMask = masks != null;
                HeaderStartIndex = 0;
                PayloadLength = dataLen;
                Masks = masks;
                var oldPayloadStartIndex = offset;
                var oldPayloadEndIndex = oldPayloadStartIndex + dataLen;
                var newBufferIdx = headLen;
                PayloadStartIndex = headLen;
                end = Buffer.Length;
                if (masks != null)
                {
                    dataId = 0;

                    for (var i = oldPayloadStartIndex; i < oldPayloadEndIndex; i++, dataId++)
                        newBuffer[newBufferIdx++] = (byte)(data[i] ^ masks[dataId % 4]);
                }
                else
                {
                    for (var i = oldPayloadStartIndex; i < oldPayloadEndIndex; i++)
                        newBuffer[newBufferIdx++] = data[i];
                }
                break;

            default:
                throw new ArgumentOutOfRangeException(nameof(saveMode), saveMode, null);
        }
    }

    public SendFrame() : base(0)
    {
    }

    public void UpdatePayloadLength(int length)
    {
        var newSize = length;
        var baseHeaderLen = HeaderLength;
        var masks = Masks;
        if (PayloadLength == length)
        {
            DataMaskAndMove(Buffer, PayloadStartIndex, PayloadEndIndex, masks, 0, 0);
            return;
        }
        var headerLen = CalculateHeaderSize(HasMask, newSize);
        if (headerLen == baseHeaderLen)
        {
            if (masks == null)
                return;
            DataMaskAndMove(Buffer, PayloadStartIndex, PayloadEndIndex, masks, 0, 0);
            return;
        }
        var move = baseHeaderLen < headerLen ? baseHeaderLen - headerLen : headerLen - baseHeaderLen;
        DataMaskAndMove(Buffer, PayloadStartIndex, PayloadEndIndex, masks, 0, move);
        PayloadLength = length;
        Masks = masks;
        PayloadStartIndex += move;
    }

    public int PayloadEndIndex => PayloadStartIndex + PayloadLength;

    public void DataMaskAndMove(byte[] buffer, int start, int end, byte[]? masks, int maskId, int move)
    {
        var hasMask = masks != null;
        switch (move)
        {
            case 0:
                if (masks == null)
                    return;
                for (var i = start; i < end; i++, maskId++)
                {
                    buffer[i] = (byte)(buffer[i] ^ masks[maskId % 4]);
                }
                break;

            case < 0:
                var newSavePos = start + move;
                if (masks != null)
                {
                    for (var i = start; i < end; i++, maskId++)
                    {
                        buffer[newSavePos++] = (byte)(buffer[i] ^ masks[maskId % 4]);
                    }
                }
                else
                {
                    for (var i = start; i < end; i++)
                    {
                        buffer[newSavePos++] = buffer[i];
                    }
                }
                break;

            case > 0:
                var newSaveEndPos = end + move - 1;
                var newSaveStartPos = start + move;
                end -= 1;
                if (masks != null)
                {
                    maskId += end - start;
                    for (var i = newSaveEndPos; i >= newSaveStartPos; i--, maskId--)
                    {
                        buffer[i] = (byte)(buffer[end--] ^ masks[maskId % 4]);
                    }
                }
                else
                {
                    for (var i = newSaveEndPos; i >= newSaveStartPos; i--)
                    {
                        buffer[i] = buffer[end--];
                    }
                }
                break;
        }
    }
}